﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Infrastructure;
using System.Security.Cryptography;
using System.Text;
using System.IO.Compression;
using RhotCMS.Models;
using System.IO;
using System.Web.Caching;
using Core.SupportEntities;
using Infrastructure.Managers;
using Utils;
using Core.Constants;

namespace RhotCMS.Filters
{
    public enum CachePolicy
    {
        NoCache = 0,
        Client = 1,
        Server = 2,
        ClientAndServer = 3
    }

    public class CacheProperties
    {
        public string CacheKey { get; set; }
        public CachePolicy CachePolicy { get; set; }
        public bool IsPreview { get; set; }
        public bool IsEtag { get; set; }
        public string NoneMatchString { get; set; }
        public int Duration { get; set; }
        public bool IsEnabled { get; set; }
    }

    public class CacheManagerAttribute : ActionFilterAttribute
    {
        #region private fields
        private string _varyByParam;
        private Cache _cache;
      
        #endregion

        public CacheManagerAttribute()
        {
            _varyByParam = "*";                  
        }

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            CacheProperties cacheProperties = new CacheProperties();
            if (filterContext.HttpContext.Items[ConstantsCache.STR_REQUESTCACHEPROPERTIES] != null)
            {
                filterContext.HttpContext.Items.Remove(ConstantsCache.STR_REQUESTCACHEPROPERTIES);
            }
            filterContext.HttpContext.Items.Add(ConstantsCache.STR_REQUESTCACHEPROPERTIES, cacheProperties);

            cacheProperties.IsEnabled = false;
            cacheProperties.CachePolicy = 0;
            cacheProperties.Duration = 0;
            cacheProperties.NoneMatchString = String.Empty;
            cacheProperties.IsEtag = false;

            var url = filterContext.HttpContext.Request.Url.AbsoluteUri;
            if (url.Contains("preview"))
            {
                cacheProperties.IsPreview = true;
                return;
            }
            else
            {
                cacheProperties.IsPreview = false;
            }
            
            var controller = (string)filterContext.RouteData.Values["group"];
            
            if (String.IsNullOrWhiteSpace(controller) && ((string)filterContext.RouteData.Values["controller"]).Equals("pages", StringComparison.InvariantCultureIgnoreCase))
                controller = "root";
            var action = (string)filterContext.RouteData.Values["name"];
            if (controller == "root" && String.IsNullOrWhiteSpace(action) && ((string)filterContext.RouteData.Values["action"]).Equals("index", StringComparison.InvariantCultureIgnoreCase))
                action = "index";

            CacheTable entity = CacheManager.CacheTable.SingleOrDefault(x => String.Equals(x.Controller, controller, StringComparison.InvariantCultureIgnoreCase) && String.Equals(x.Action, action, StringComparison.InvariantCultureIgnoreCase));

            if (entity == null)
            {                
                return;
            }

            cacheProperties.IsEnabled = entity.IsEnabled;

            if (!cacheProperties.IsEnabled)
            {
                return;
            }

            cacheProperties.IsEtag = entity.IsEtag;
            cacheProperties.Duration = entity.Duration;

           
            if (entity.CachePolicy == null || entity.CachePolicy == 0)
            {
                cacheProperties.CachePolicy = CachePolicy.NoCache;
            }
            else
            {
                cacheProperties.CachePolicy = (CachePolicy)Enum.ToObject(typeof(CachePolicy), (int)entity.CachePolicy);
            }

            if (cacheProperties.CachePolicy == CachePolicy.Server || cacheProperties.CachePolicy == CachePolicy.ClientAndServer)
            {
                var cacheKey = GenerateKey(filterContext);
                cacheProperties.CacheKey = cacheKey;
                _cache = filterContext.HttpContext.Cache;
                object cachedData = _cache.Get(cacheKey);
                if (cachedData != null)
                {
                    filterContext.Result = new EmptyResult();
                }
            }

            cacheProperties.NoneMatchString = filterContext.HttpContext.Request.Headers["If-None-Match"];

            if (filterContext.IsChildAction || filterContext.HttpContext.Request.RequestType != "GET")
            {
                return;
            }

            if (cacheProperties.CachePolicy == CachePolicy.Client || cacheProperties.CachePolicy == CachePolicy.ClientAndServer)
            {
                if (cacheProperties.Duration <= 0) return;

                HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
                TimeSpan cacheDuration = TimeSpan.FromSeconds(cacheProperties.Duration);
                cache.SetCacheability(HttpCacheability.Public);
                cache.SetExpires(DateTime.Now.Add(cacheDuration));
                cache.SetMaxAge(cacheDuration);
                cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
            }

            if (entity.IsGzip)
            {
                HttpRequestBase request = filterContext.HttpContext.Request;
                string acceptEncoding = request.Headers["Accept-Encoding"];
                if (!string.IsNullOrEmpty(acceptEncoding))
                {
                    acceptEncoding = acceptEncoding.ToUpperInvariant();
                    HttpResponseBase response = filterContext.HttpContext.Response;
                    if (acceptEncoding.Contains("GZIP"))
                    {
                        response.AppendHeader("Content-encoding", "gzip");
                        response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
                    }
                    else
                        if (acceptEncoding.Contains("DEFLATE"))
                        {
                            response.AppendHeader("Content-encoding", "deflate");
                            response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
                        }
                }
            }


        }

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {

        }

        public override void OnResultExecuting(ResultExecutingContext filterContext)
        {
            var cacheProperties = (CacheProperties)filterContext.HttpContext.Items[ConstantsCache.STR_REQUESTCACHEPROPERTIES];

            if (cacheProperties == null)
                throw new ArgumentNullException(String.Format("Не удалось получить filterContext.HttpContext.Items[{0}]", ConstantsCache.STR_REQUESTCACHEPROPERTIES));

            if (cacheProperties.IsPreview)
            {
                return;
            }
            // Server-side caching?           

            if (cacheProperties.CachePolicy == CachePolicy.Server || cacheProperties.CachePolicy == CachePolicy.ClientAndServer)
            {
                // Fetch Cache instance
                _cache = filterContext.HttpContext.Cache;

                // Fetch cached data
                object cachedData = _cache.Get(cacheProperties.CacheKey);
                if (cachedData != null)
                {
                    string etag = String.Empty;

                    if (cacheProperties.IsEtag)
                    {
                        etag = CreateETag((string)cachedData);
                        filterContext.HttpContext.Response.AddHeader("ETag", etag);
                    }

                    if (cacheProperties.IsEtag && etag == cacheProperties.NoneMatchString)
                    {
                        filterContext.HttpContext.Response.StatusCode = 304;
                        filterContext.HttpContext.Response.AddHeader("Content-Length", "0");
                    }
                    else
                    {
                        filterContext.HttpContext.Response.Write(cachedData);
                    }
                }
                else
                {
                    HttpResponseBase response = filterContext.HttpContext.Response;
                    var cacheFilter = new CacheFilter(filterContext, response.Filter, cacheProperties.CacheKey, cacheProperties.IsEtag, cacheProperties.NoneMatchString, cacheProperties.Duration);
                    response.Filter = cacheFilter;
                }
            }
            else if (cacheProperties.CachePolicy == CachePolicy.Client && cacheProperties.IsEtag)
            {
                HttpResponseBase response = filterContext.HttpContext.Response;
                var cacheFilter = new CacheFilter(filterContext, response.Filter, String.Empty, cacheProperties.IsEtag, cacheProperties.NoneMatchString, cacheProperties.Duration);
                response.Filter = cacheFilter;
            }
        }

        public string GenerateKey(ControllerContext filterContext)
        {
            StringBuilder cacheKey = new StringBuilder();
            // Controller + action
            cacheKey.Append(filterContext.Controller.GetType().FullName.ToLower());
            if (filterContext.RouteData.Values.ContainsKey("action"))
            {
                cacheKey.Append("_");
                cacheKey.Append(filterContext.RouteData.Values["action"].ToString().ToLower());
            }
            // Variation by parameters
            List<string> varyByParam = _varyByParam.Split(';').ToList();
            if (!string.IsNullOrEmpty(_varyByParam))
            {
                foreach (KeyValuePair<string, object> pair in filterContext.RouteData.Values)
                {
                    if (_varyByParam == "*" || varyByParam.Contains(pair.Key))
                    {
                        cacheKey.Append("_");
                        cacheKey.Append(pair.Key.ToLower());
                        cacheKey.Append("=");
                        cacheKey.Append(pair.Value.ToString().ToLower());
                    }
                }
            }

            var query = filterContext.HttpContext.Request.Url.Query;

            if (!String.IsNullOrWhiteSpace(query))
                cacheKey.Append(query);

            return cacheKey.ToString();
        }

        /// <summary> 
        /// Construct the ETag 
        /// </summary> 
        /// <param name="context"></param> 
        /// <returns></returns> 
        private static string CreateETag(string s)
        {
            return String.Format("\"{0}\"", CalculateMD5Hash(String.Format("{0}", s)));
        }

        /// <summary> 
        /// Helper to make an MD5 hash 
        /// </summary> 
        /// <param name="input"></param> 
        /// <returns></returns> 
        private static string CalculateMD5Hash(string input)
        {
            using (MD5 md5 = MD5.Create())
            {
                byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
                byte[] hash = md5.ComputeHash(inputBytes);
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < hash.Length; i++)
                {
                    sb.Append(hash[i].ToString("X2"));
                }
                return sb.ToString();
            }
        }

        public class CacheFilter : Stream
        {
            private Stream _stream;
            //private ILogger _logger;          
            private byte[] _bufferSum;
            private byte[] _htmlStartTagBytes;
            private byte[] _htmlEndTagBytes;
            private string _key;
            private bool _isEtag;
            private int _duration;
            private string _oldEtag;
            private ResultExecutingContext _context;

            public CacheFilter(ResultExecutingContext context, Stream stream, string key, bool isEtag, string oldEtag, int Duration)
            {
                //_logger = ServiceLocator.Current.GetInstance<ILogger>();                
                _context = context;
                _stream = stream;
                _key = key;
                _isEtag = isEtag;
                _oldEtag = oldEtag;
                _duration = Duration;

                _htmlStartTagBytes = Encoding.Default.GetBytes("<html>");
                _htmlEndTagBytes = Encoding.Default.GetBytes("</html>");
            }

            //overridden functions omitted for clarity. See above blog post.

            public override void Write(byte[] buffer, int offset, int count)
            {
                // capture the data and convert to string                
                byte[] data = new byte[count];
                Buffer.BlockCopy(buffer, offset, data, 0, count);
              
                if (_bufferSum == null)
                {
                    _bufferSum = new byte[buffer.Length];
                    Buffer.BlockCopy(buffer, 0, _bufferSum, 0, buffer.Length);
                }
                else
                {
                    var tempBufferSumm = new byte[_bufferSum.Length];
                    Buffer.BlockCopy(_bufferSum, 0, tempBufferSumm, 0, _bufferSum.Length); 
                    _bufferSum = new byte[tempBufferSumm.Length + buffer.Length];
                    Buffer.BlockCopy(tempBufferSumm, 0, _bufferSum, 0, tempBufferSumm.Length);
                    Buffer.BlockCopy(buffer, 0, _bufferSum, tempBufferSumm.Length, buffer.Length);                                       
                }

                if (_bufferSum.ContainsBytes(ref _htmlStartTagBytes) && _bufferSum.ContainsBytes(ref _htmlEndTagBytes))
                {
                    var unicodeHtml = Encoding.UTF8.GetString(_bufferSum);

                    bool isResponse = true;

                    if (_isEtag)
                    {
                        var etag = CreateETag((string)unicodeHtml);
                        if (!String.IsNullOrWhiteSpace(_oldEtag) && etag == _oldEtag)
                        {
                            _context.HttpContext.Response.StatusCode = 304;
                            _context.HttpContext.Response.AddHeader("Content-Length", "0");
                            isResponse = false;
                        }
                        else
                        {
                            _context.HttpContext.Response.AddHeader("ETag", etag);
                        }
                    }

                    if (!String.IsNullOrWhiteSpace(_key) && _context.HttpContext.Cache.Get(_key) == null)
                    {
                        _context.HttpContext.Cache.Add(
                        _key,
                        unicodeHtml,
                        null,
                        DateTime.Now.AddSeconds(_duration),
                        Cache.NoSlidingExpiration,
                        CacheItemPriority.AboveNormal,
                        null);
                    }

                    if (isResponse)
                    {
                        _stream.Write(_bufferSum, 0, _bufferSum.GetLength(0));
                    }
                }
            }            

            public override bool CanRead
            {
                get { throw new NotImplementedException(); }
            }

            public override bool CanSeek
            {
                get { throw new NotImplementedException(); }
            }

            public override bool CanWrite
            {
                get { return true; }
            }

            public override void Close()
            {
                _stream.Close();
            }

            public override void Flush()
            {
                _stream.Flush();
            }

            public override long Length
            {
                get { throw new NotImplementedException(); }
            }

            public override long Position
            {
                get
                {
                    throw new NotImplementedException();
                }
                set
                {
                    throw new NotImplementedException();
                }
            }

            public override int Read(byte[] buffer, int offset, int count)
            {
                throw new NotImplementedException();
            }

            public override long Seek(long offset, SeekOrigin origin)
            {
                throw new NotImplementedException();
            }

            public override void SetLength(long value)
            {
                throw new NotImplementedException();
            }
        }
    }
}